########################################################################
#  Searx-qt - Lightweight desktop application for SearX.
#  Copyright (C) 2020  CYBERDEViL
#
#  This file is part of Searx-qt.
#
#  Searx-qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

import requests
from requests.exceptions import HTTPError, ConnectionError, Timeout
from copy import deepcopy
import json


class SearchErrorType:
    HttpError = 0
    ConnectionError = 1
    Timeout = 3
    WrongStatus = 4
    DecodeError = 5
    NoResults = 6


class SearchResult:
    def __init__(self, response=None, err="", errType=None):
        self._response = response
        self._err = err
        self._errType = errType

        if errType is None and response.status_code != 200:
            self._errType = SearchErrorType.WrongStatus
            self._err = "WrongStatus: {0}".format(self._response.status_code)
        else:
            try:
                self.json()
            except json.JSONDecodeError as err:
                self._errType = SearchErrorType.DecodeError
                self._err = "DecodeError: `{0}`".format(err)

        if not len(self.json().get('results', {})):
            self._errType = SearchErrorType.NoResults
            self._err = "NoResults: got: `{0}`".format(self.json())

    def __bool__(self):
        return not self.failed()

    def errorType(self): return self._errType

    def error(self): return self._err

    def failed(self):
        if self._errType is not None:
            return True
        return False

    def json(self):
        if self.errorType() is not None:
            return {}
        return json.loads(self._response.content)


class Categories:
    types = {
        'general': ('General', 'category_general'),
        'files': ('Files', 'category_files'),
        'images': ('Images', 'category_images'),
        'videos': ('Videos', 'category_videos'),
        'it': ('IT', 'category_it'),
        'map': ('Location', 'category_map'),
        'music': ('Music', 'category_music'),
        'news': ('News', 'category_news'),
        'science': ('Science', 'category_science'),
        'social': ('Social', 'category_social media')
    }

    def __init__(self):
        self._options = {}
        self.__makeOptions()

    def __makeOptions(self):
        self._options.clear()
        for key, t in self.types.items():
            self._options.update({key: False})

    def get(self, key):
        return self._options[key]

    def set(self, key, state):
        """
        @param key: One of the keys in Categories.types
        @type key: str

        @param state: Enabled / disabled state
        @type state: bool
        """
        self._options[key] = state

    def dict(self):
        newDict = {}
        for key, state in self._options.items():
            if state:
                newDict.update({self.types[key][1]: 'on'})
        return newDict


class Engines(list):
    def __init__(self):
        list.__init__(self)

    def dict(self):
        if not self:
            return {}
        return {
            'engines': ",".join(self)
        }


class SearX:
    Periods = {
        '': 'Anytime',
        'day': 'Last day',
        'week': 'Last week',
        'month': 'Last month',
        'year': 'Last year'
    }

    # https://github.com/asciimoo/searx/blob/master/searx/languages.py
    Languages = {
        '': 'No language',
        'all': 'Default language',
        'af-NA': 'Afrikaans - af-NA',
        'ca-AD': 'Català - ca-AD',
        'da-DK': 'Dansk - da-DK',
        'de': 'Deutsch - de',
        'de-AT': 'Deutsch (Österreich) - de-AT',
        'de-CH': 'Deutsch (Schweiz) - de-CH',
        'de-DE': 'Deutsch (Deutschland) - de-DE',
        'et-EE': 'Eesti - et-EE',
        'en': 'English - en',
        'en-AU': 'English (Australia) - en-AU',
        'en-CA': 'English (Canada) - en-CA',
        'en-GB': 'English (United Kingdom) - en-GB',
        'en-IE': 'English (Ireland) - en-IE',
        'en-IN': 'English (India) - en-IN',
        'en-NZ': 'English (New Zealand) - en-NZ',
        'en-PH': 'English (Philippines) - en-PH',
        'en-SG': 'English (Singapore) - en-SG',
        'en-US': 'English (United States) - en-US',
        'es': 'Español - es',
        'es-AR': 'Español (Argentina) - es-AR',
        'es-CL': 'Español (Chile) - es-CL',
        'es-ES': 'Español (España) - es-ES',
        'es-MX': 'Español (México) - es-MX',
        'fr': 'Français - fr',
        'fr-BE': 'Français (Belgique) - fr-BE',
        'fr-CA': 'Français (Canada) - fr-CA',
        'fr-CH': 'Français (Suisse) - fr-CH',
        'fr-FR': 'Français (France) - fr-FR',
        'hr-HR': 'Hrvatski - hr-HR',
        'id-ID': 'Indonesia - id-ID',
        'it-IT': 'Italiano - it-IT',
        'sw-KE': 'Kiswahili - sw-KE',
        'lv-LV': 'Latviešu - lv-LV',
        'lt-LT': 'Lietuvių - lt-LT',
        'hu-HU': 'Magyar - hu-HU',
        'ms-MY': 'Melayu - ms-MY',
        'nl': 'Nederlands - nl',
        'nl-BE': 'Nederlands (België) - nl-BE',
        'nl-NL': 'Nederlands (Nederland) - nl-NL',
        'nb-NO': 'Norsk Bokmål - nb-NO',
        'pl-PL': 'Polski - pl-PL',
        'pt': 'Português - pt',
        'pt-BR': 'Português (Brasil) - pt-BR',
        'pt-PT': 'Português (Portugal) - pt-PT',
        'ro-RO': 'Română - ro-RO',
        'sk-SK': 'Slovenčina - sk-SK',
        'sl-SI': 'Slovenščina - sl-SI',
        'sr-RS': 'Srpski - sr-RS',
        'fi-FI': 'Suomi - fi-FI',
        'sv-SE': 'Svenska - sv-SE',
        'vi-VN': 'Tiếng Việt - vi-VN',
        'tr-TR': 'Türkçe - tr-TR',
        'is-IS': 'Íslenska - is-IS',
        'cs-CZ': 'Čeština - cs-CZ',
        'el-GR': 'Ελληνικά - el-GR',
        'be-BY': 'Беларуская - be-BY',
        'bg-BG': 'Български - bg-BG',
        'ru-RU': 'Русский - ru-RU',
        'uk-UA': 'Українська - uk-UA',
        'hy-AM': 'Հայերեն - hy-AM',
        'he-IL': 'עברית - he-IL',
        'ar-SA': 'العربية - ar-SA',
        'fa-IR': 'فارسی - fa-IR',
        'th-TH': 'ไทย - th-TH',
        'zh': '中文 - zh',
        'zh-CN': '中文 (中国) - zh-CN',
        'zh-TW': '中文 (台灣) - zh-TW',
        'ja-JP': '日本語 - ja-JP',
        'ko-KR': '한국어 - ko-KR'
    }

    def __init__(self):
        self._url = ""
        self._categories = Categories()
        self._engines = Engines()

        self._lang = ''
        self._pageno = ''  # int formated as string
        self._timeRange = ''  # '', 'day', 'week', 'month' or 'year'

        self._kwargs = {
            'data': {
                'q': '',
                'format': 'json'
            },
        }

    @property
    def categories(self): return self._categories

    @property
    def engines(self): return self._engines

    @property
    def url(self):
        """
        @return: Instance url
        @rtype: str
        """
        return self._url

    @url.setter
    def url(self, url):
        """
        @param url: Instance url
        @type url: str
        """
        self._url = url

    @property
    def query(self):
        """
        @return: Search query
        @rtype: str
        """
        return self._kwargs['data']['q']

    @query.setter
    def query(self, q):
        """
        @param q: Search query
        @type q: str
        """
        print("Set query: {}".format(q))
        self._kwargs['data']['q'] = q

    @property
    def lang(self):
        """
        @return: Language code
        @rtype: str
        """
        return self._lang

    @lang.setter
    def lang(self, lang):
        """
        @param lang: Language code
        @type lang: str
        """
        self._lang = lang

    @property
    def pageno(self):
        """
        @return: Page number
        @rtype: int
        """
        return int(self._pageno)

    @pageno.setter
    def pageno(self, i):
        """
        @param i: Page number
        @type i: int
        """
        self._pageno = str(i)

    @property
    def timeRange(self):
        """
        @return: Search time range ('', 'day', 'week', 'month' or 'year')
        @rtype: str
        """
        return self._timeRange

    @timeRange.setter
    def timeRange(self, value):
        """
        @param value: Key from SearX.Periods
        @type value: str
        """
        self._timeRange = value

    @property
    def requestArgs(self):
        """ Returns current data that will be send with the POST
        request used for the search operation. The search query,
        language, page-number and enabled categories/engines.

        @rtype: dict
        """
        kwargs = deepcopy(self._kwargs['data'])
        kwargs.update(self.categories.dict())
        kwargs.update(self.engines.dict())
        if self.lang:
            kwargs.update({'lang': self.lang})
        if self.pageno:
            kwargs.update({'pageno': self.pageno})
        if self.timeRange:
            kwargs.update({'timeRange': self.timeRange})
        return kwargs

    def search(self, requestKwargs={}):
        """ Preform search operation with current set values.

        @param requestKwargs: Keyword arguments that will be passed to
                              the requests module it's post method.
                              Probably to set a proxy, set timeout or to
                              verify SSL or not; see
                              https://requests.readthedocs.io for what is
                              possible. You probably want to make sure that
                              you don't declare the key `data` since it
                              will be overwritten.
        @type requestKwargs: dict

        @returns: The result of this search.
        @rtype: SearchResult
        """
        requestKwargs.update({'data':  self.requestArgs})

        # TODO using this for debugging.
        print(
            "Search request to {0} with requests kwargs: {1}"
            .format(self._url, requestKwargs)
        )

        response = None
        err = ""
        errType = None

        try:
            response = requests.post(self._url, **requestKwargs)
        except HTTPError as e:
            print("Search failed! HTTPError: {0}".format(e))
            errType = SearchErrorType.HttpError
            err = e
        except ConnectionError as e:
            print("Search failed! ConnectionError: {0}".format(e))
            errType = SearchErrorType.ConnectionError
            err = e
        except Timeout as e:
            print("Search failed! Timeout: {0}".format(e))
            errType = SearchErrorType.Timeout
            err = e

        return SearchResult(response=response, err=err, errType=errType)
